STATS.NBA.COM 크롤링(스크래핑) by using JSON in Python

STATS.NBA.COM 크롤링(스크래핑) by using JSON in Python



STATS.NBA.COM 파싱하기 드러워서 내가 쓴다…

크롤링인지 스크래핑인지 만만치 않다는 사실을 깨달았읍니다…


1
Question: 아래 URL의 NBA 데이터를 크롤링하여 판다스 데이터 프레임으로 나타내세요.

해당 페이지 링크는 아래과 같다.

http://stats.nba.com/teams/traditional/?sort=GP&dir=-1


그리고 우리가 Pandas로 구현해야 하는

이 테이블을 JSON 파일 형태로 html Pasing하여 출력하도록 하겠다.

1. STATS.NBA.COM 해당 페이지의 숨어있는 JSON 파일 찾기

위에 표시된 링크에 들어가면 이러한 화면이 보인다.


이곳에서 키보드의 F12 를 누르면, 오른쪽에 개발자 도구가 뜨게 되는데, Network 에 들어가서 XHR 탭을 클릭하고 F5 를 눌러보자.


Name 탭에 5가지의 Request 결과가 뜨는데, 이 중 4번째인 leaguedashteamstats?Conference= 로 시작하는 데이터를 클릭하고 오른쪽 탭의 resultSets 를 계속 펼쳐보면 rowSet 묶음에 STATS.NBA.COM 페이지에서 봤던 익숙한 NBA 팀 이름들이 뜬다.

개발자 도구 안에서도 JSON의 구조를 파악하고 Pasing이 가능하지만, 우리는 JSON 파일을 예쁘게 볼 수 있는 크롬 앱을 사용해보도록 하겠다. 필자가 쓰는 JSON Viewer는 Json Viewer다. 이 곳에서 크롬 앱에 추가할 수 있다. 일단 추가하면 JSON 파일을 열 때 알아서 예쁘게 보여준다.

그럼 이제 leaguedashteamstats?어쩌구를 더블클릭해서 열어보자… 혹은 이 링크를 누르면 JSON 형태의 웹 페이지가 보인다.


이것이 바로 우리가 데이터를 뽑아내야 할 JSON 웹 페이지의 구조이다. 오른쪽 위에 보이는 위아래 화살표 버튼을 클릭하면


이런 식으로 접히게 된다. 정신산란할 땐 접어놓고 복습호흡을 내뱉으며 하나하나 뜯어가며 코드를 분해하도록 하자.

이정도 정보를 얻었으면, 코드를 조금 작성해보자. IDE는 Jupyter Notebook을 사용하였다.


1
2
3
import requests
import json
import pandas as pd

import할 패키지는 총 3가지이다.

  • 원하는 웹 페이지에 Request(요청)를 보내 html 결과를 받기 위한 requests
  • html 안의 text 결과를 받아와서 데이터로 사용하기 위한 json
  • 그리고 DataFrame으로 데이터를 테이블 형태로 만들어줄 pandas.
    • as pd 를 붙이는 이유는 pandas를 매번 입력하기 귀찮아서 pd로 퉁쳐주기 위한 것이다.


1
url = "http://stats.nba.com/stats/leaguedashteamstats?Conference=&DateFrom=&DateTo=&Division=&GameScope=&GameSegment=&LastNGames=0&LeagueID=00&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2017-18&SeasonSegment=&SeasonType=Playoffs&ShotClockRange=&StarterBench=&TeamID=0&VsConference=&VsDivision="

우선 url 변수에 JSON 파일의 url 주소를 입력해준다. 그리고 아래 get_data 함수의 Parameter로 써준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
def get_data(url):
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)\
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}
response = requests.get(url, headers=headers)
json_info = json.loads(response.text)
stats = json_info["resultSets"][0]["rowSet"]
df = pd.DataFrame(columns=[\
"TEAM","GP","W","L","WIN%","MIN","PTS","FGM","FGA",\
"FG%","3PM","3PA","3P%","FTM","FTA","FT%","OREB","DREB",\
"REB","AST","TOV","STL","BLK","BLKA","PF","PFD","+/-"])
for stat in stats:
df.loc[len(df)] = {
"TEAM":stat[1],
"GP":stat[2],
"W":stat[3],
"L":stat[4],
"WIN%":stat[5],
"MIN":stat[6],
"PTS":stat[26],
"FGM":stat[7],
"FGA":stat[8],
"FG%":stat[9],
"3PM":stat[10],
"3PA":stat[11],
"3P%":stat[12],
"FTM":stat[13],
"FTA":stat[14],
"FT%":stat[15],
"OREB":stat[16],
"DREB":stat[17],
"REB":stat[18],
"AST":stat[19],
"TOV":stat[20],
"STL":stat[21],
"BLK":stat[22],
"BLKA":stat[23],
"PF":stat[24],
"PFD":stat[25],
"+/-":stat[27],
}
df = df.sort_values(by=["GP"], ascending=False)
df = df.reset_index(drop=True)
df.index += 1
return df

하나씩 뜯어가면서 설명하겠다. 아래 코드는 위의 코드와 중복된다. 그러니 위의 코드만 입력하면 된다. 그냥 설명용으로 작성한다.


1
2
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)\
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}

STATS.NBA.COM은 아마도 Scraping을 하기 위해 필요로 하는 웹 브라우저 버전이 따로 있는 것 같다. 필자는 Chrome 67.0.3396.87 64Bit 버전을 사용하는데, 61.0.3163.100 버전을 사용하는 것 처럼 속여줘야 데이터를 가져오는 듯 하다. 그냥 구글링 해서 나의 고통을 가지고 있는 분의 질문에 대한 해답에 이런 늬앙스의 글이 있었다. 일단 headers 변수에 해당 내용을 입력해주자.


1
2
response = requests.get(url, headers=headers)
json_info = json.loads(response.text)

response 변수에 아까 입력한 url 주소의 html을 get 방식으로 받겠다고 요청을 보낸다. json_info 변수에 response변수로 받은 html내용의 text를 load하겠다는 명령어를 입력해준다.


1
stats = json_info["resultSets"][0]["rowSet"]

가장 고생을 많이 한 부분이다. stats 변수에 rowSet의 데이터가 입력되어야 하는데(NBA 16팀), json_info["resultSets"]["rowSet"] 으로 썼더니 'str' object has no attribute 'read' 에러가 떴다. [0]을 중간에 추가해줘야한다. JSON Viewer로 보면 이 구조가 잘 안보이고, 오히려 개발자 도구에서 본 구조에서 힌트를 얻었다.


1
2
3
4
df = pd.DataFrame(columns=[\
"TEAM","GP","W","L","WIN%","MIN","PTS","FGM","FGA",\
"FG%","3PM","3PA","3P%","FTM","FTA","FT%","OREB","DREB",\
"REB","AST","TOV","STL","BLK","BLKA","PF","PFD","+/-"])

df 변수 주소값에 pandas DataFrame을 할당해준다. 상단 columns에 TEAM, GP, W 등의 변수들을 추가해준다. 그럼 아래와 같은 형태로 나타난다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
for stat in stats:
df.loc[len(df)] = {
"TEAM":stat[1],
"GP":stat[2],
"W":stat[3],
"L":stat[4],
"WIN%":stat[5],
"MIN":stat[6],
"PTS":stat[26],
"FGM":stat[7],
"FGA":stat[8],
"FG%":stat[9],
"3PM":stat[10],
"3PA":stat[11],
"3P%":stat[12],
"FTM":stat[13],
"FTA":stat[14],
"FT%":stat[15],
"OREB":stat[16],
"DREB":stat[17],
"REB":stat[18],
"AST":stat[19],
"TOV":stat[20],
"STL":stat[21],
"BLK":stat[22],
"BLKA":stat[23],
"PF":stat[24],
"PFD":stat[25],
"+/-":stat[27],
}

for문으로 각 행에 column과 일치하는 데이터를 순서대로 넣어준다. 나머지는 다 순서대로인데, PTS 탭만 stat[26] 데이터를 차용한다. 헷갈리지 말자. 여기까지 입력했으면 아래와 같이 df 변수의 DataFrame이 만들어진다.


1
2
3
4
    df = df.sort_values(by=["GP"], ascending=False)
df = df.reset_index(drop=True)
df.index += 1
return df

만들어진 df를 GP column 기준으로 정렬해주고, ascending=False 명령어로 내림차순을 적용시켜준다. 그럼 아래와 같이 인덱스값이 흐트러지게 된다.

df = df.reset_index(drop=True) 명령으로 인덱스를 리셋해주고, 기존 인덱스는 drop해버린다. 더하여 인덱스가 0으로 시작하는데, df.index += 1 명령으로 인덱스에 모든 값을 1씩 더해준다. indent 위치를 주의하자. 이전 명령들의 indent보다 space 4칸이 적다. 그래야 모든 index에 적용된다. 마지막으로 df를 return해준다.

결과는…


img

위와 같다.

홈페이지의 이 표와 비교하면 내용은 완벽히 같다.. 추후 STATS.NBA.COM의 JSON url 값이나 column 고유값이 변하지 않는 이상, 팀간 GP 값의 변동이 있더라도 get_data 함수가 알아서 Parsing 해줄 것이다.

초기에 http://stats.nba.com/teams/traditional/?sort=GP&dir=-1 URL에서 보듯이, 이 예제에서는 GP값을 기준으로 정렬했는데 df = df.sort_values(by=["GP"], ascending=False) 명령에서 "GP" 부분을 다른 column 값으로 수정하면 다른 기준으로 정렬 될 것이다.


스크래핑, 혹은 크롤링이라고 불리는 작업이 쉽지 않다는 것을 깨달은 하루였다… T-T

소요 시간은 약 4시간정도. 그리고 이 글을 적는 것은 1시간 반 정도 걸렸다.ㅋㅋㅋㅋ

무려 5시간 넘게 뺏어간 놈이다.. 하지만 추후 크롤링을 더 수월하게 할 수 있을 것이라는 자신감이 생겼다.


다음 글 예고


  • Selenium을 이용한 네이버 기사 10페이지의 제목 리스트 크롤링!